目前的 Todo List 在輸入欄位為空白時仍然可以送出。今天,我們要來修正這個問題,並為其新增錯誤提示訊息。此外,為了練習我們在 Day08 提到的 Literal Types,我們將在 Todo 成功建立時顯示成功提示訊息。
首先,在 components 資料夾中加入檔案 Message.tsx,並建立 Message 元件。Message 元件將放置在 App.tsx 中,並接收來自 App 的 props,透過這些 props 來控制顯示與否以及顯示的內容。因此,我們將 type 定義在 App.tsx 並匯出,以便 Message 元件可以共用這個型別。
由於我們的提示訊息只會有「成功」和「失敗」兩種狀態,因此可以使用 Literal Types 來定義 mode 的型別:
export type MessageDetails = {
visible: boolean
message: string
mode: 'error' | 'success'
}
接著,在 App.tsx 中建立狀態,並定義其初始值:
const [MessageDetails, setMessageDetails] = useState<MessageDetails>({
visible: false,
message: '',
mode: 'error',
})
由於 Message 元件會使用 fixed 定位,它可以放置在 <main> 中的任意位置。這裡我們選擇將它放置在頁面底部,並傳入定義好的狀態作為 props:
...
<main className='w-[500px] h-[100dvh] portrait:w-[90%] flex flex-col'>
<Header image={{ src: logo, alt: 'logo' }}>
<h1>Todo List</h1>
</Header>
<CreateTodo onCreateTodo={createTodoHandler} />
<TodoList todos={todos} onDeleteTodo={deleteTodoHandler} />
<Message
visible={messageDetails.visible}
mode={messageDetails.mode}
message={messageDetails.message}
/>
</main>
...
前往 Message.tsx 並匯入我們在 App.tsx 中定義的 MessageDetails 型別:
import { type MessageDetails } from '../App'
export default function Message({ visible, message, mode }: MessageDetails) {
return (
<div
className={`${mode === 'error' ? 'bg-red-500' : 'bg-green-500'} ${
visible ? 'flex' : 'hidden'
} rounded-[5px] p-[10px] fixed bottom-[20px] left-[20px]`}
>
<p className='text-[20px]'>{message}</p>
</div>
)
}
打開瀏覽器,試著手動更改 App 元件中 messageDetails 的初始值,你應該可以看到像下方截圖中的這樣畫面:

接著,回到 App 元件,在 createTodoHandler 函式中新增提示訊息的相關邏輯,以處理成功和錯誤狀態:
const createTodoHandler = (title: string) => {
if (title.trim().length === 0) {
setMessageDetails({
visible: true,
message: 'Input cannot be empty!',
mode: 'error',
})
return
}
const newTodo: TodoItem = {
id: Math.random(),
title: title,
isFinished: false,
}
setTodos((prevTodos) => [...prevTodos, newTodo])
setMessageDetails({
visible: true,
message: 'Todo created successfully!',
mode: 'success',
})
}
目前提示訊息的顯示功能已經完成,接下來,我們要處理提示訊息的自動隱藏。Message 元件的顯示狀態儲存在 App.tsx 中的 messageDetails 狀態,因此,我們需要將更新狀態的 setMessageDetails 函式傳遞給 Message 元件:
<Message
visible={messageDetails.visible}
mode={messageDetails.mode}
message={messageDetails.message}
onMessageVisible={setMessageDetails}
/>
在 Message 元件中,我們會使用先前在 Day08 提到的合併型別方法加入 onMessageVisible。為了增強程式碼的可讀性,這裡我們將型別定義獨立提出來,並設置 onMessageVisible 的參數型別為 SetStateAction,泛型參數則使用我們先前定義的 MessageDetails:
import { type SetStateAction } from 'react'
type MessageProps = MessageDetails & {
onMessageVisible: (value: SetStateAction<MessageDetails>) => void
}
最後,加入 useEffect,當 visible 為 true 時,觸發計時器,在三秒後自動隱藏提示訊息,並清除上一次的計時器,避免計時器累加造成記憶體的負擔:
useEffect(() => {
if (visible) {
const timer = setTimeout(() => {
onMessageVisible((prev: MessageDetails) => ({
...prev,
visible: false,
}))
}, 3000)
return () => clearTimeout(timer)
}
}, [visible])
在最一開始,我們提到了使用 Literal Types 定義型別:
export type MessageDetails = {
visible: boolean
message: string
mode: 'error' | 'success'
}
若有一個元件不需要 mode,而我們在使用 MessageDetails 型別時沒有傳遞 mode 給該元件,那麼 TypeScript 會報錯。在這種情況下,我們可以有兩種解決方法。
第一種寫法是將 mode 設為可選的,並允許其值為 undefined,這樣元件即使沒有傳遞 mode 也不會報錯:
export type MessageDetails = {
visible: boolean
message: string
mode: 'error' | 'success' | undefined
}
第二種寫法是在 mode 後加上 ?,這樣就會讓 mode 成為可選的屬性:
export type MessageDetails = {
visible: boolean
message: string
mode?: 'error' | 'success'
}
兩種寫法效果相同,可以根據個人喜好選擇。